Daily Task Structure with eInk Tablet and nixOS (Part 2)

"Van Gogh Museum - Almond Blossom 1890" by MicheleLovesArt is licensed under CC BY-SA 2.0. To view a copy of this license, visit https://creativecommons.org/licenses/by-sa/2.0/?ref=openverse.
Building and Running the App
So we used fenix to setup our "nixed" Rust project. What packages did we actually consume in our rust code?
chrono = "0.4.41"
reqwest_dav = { version = "0.2.1", features = ["rustls-tls"], default-features = false }
serde = "1.0.219"
serde_json = "1.0.142"
tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] }
Not as slim as i would have liked to. We need serde to get the WebDAV data out of the reqwest_dav response and tokio because of reqwest and because I was too lazy to figure out how to run reqwest without tokio - if this is even a thing.
And chrono for obvious reasons.
Note that default-features is set to false and I am explicitly consuming rustls-tls. This was as side-effect of using nix for this project. How so you might ask?
When this get's build on my MacBook Pro, it worked just fine without that dependency configuration, however, once I tried to build that flake on my RPi it complained about not finding OpenSSL. Instead of making libopenssl available to the flake (I have not checked how do to it, I assume it is "just" specifying "buildInputs" or something similar in the flake or the fenix derivation and supplying pkgs.libssl-or-something), I decided to just use rustls-tls.
Once this was done, the sync loop was running just fine. The compile time of the rather "simple" application on the RPi was abysmal. Lucky me Determinate Systems is rolling out native cross platform builds for determinate nix (I am in the dev preview now, but could not test it for this specific case), so this will hopefully not be a pain in the ass in the future.
Now to the less nice part. Yes the WebDAV CLI/libopenssl part was not the worst part.
The nixOS installation on the RPi is using stock nixOS KDE Plasma 6 (wayland). So I thought, let's not reinvent the wheel and use the existing tools. I.e., use the builtin PDF app (okular) and start / manage the GUI app(s) using the rust program. I use the following tools for that and call them from my rust program:
- kscreen-doctor (hypothetically, but I had to temporarily remove it due some quirks when running with systemd, which is NOT systemd's fault)
- kdotool (abstract talking to ?? for us, not sure if it's the compositor or window manager)
systemd-run
Invoking any of the above roughly looks like this (the env variable is important):
let output = std::process::Command::new("systemd-run")
.args([
"--user",
"--scope",
"okular",
"--page",
&page.to_string(),
p.to_str().unwrap(),
])
.env("QT_QPA_PLATFORM", "wayland")
.spawn()
.expect("systemd-run Command did not run successfully");
With the exception in the above example, where we return a Child instead of directly reading the stdout/stderr. Why are we doing this? Because if you do this on okular, when starting via systemd-run with not special args/flags it is blocking w.r.t. to stdout/stderr. So I decided to just be happy with keeping around the child process and ignoring stdout.
kdotool is being used to resize and position the okular windows (I have to display at least two PDFs side-by-side).
Some excerpt from that part:
let mut offset: u16 = 0;
for wnd in windows.iter().take(2) {
println!("Moving {}", wnd);
let res = run_kdotool(vec![
"windowmove",
wnd,
format!("{}", 0 + offset).as_str(),
format!("{}", 0).as_str(),
]);
offset += width.div_ceil(2);
println!("{}", res);
println!("Resizing {}", wnd);
let res = run_kdotool(vec![
"windowsize",
wnd,
format!("{}", width.div_ceil(2)).as_str(),
format!("{}", height).as_str(),
]);
println!("{}", res);
}
Works just fine.
No Keyboard, No Mouse, Just Boot And Be Happy
The app "works", it might not be the most beautiful and optimized piece of software, but it does it's job.
Well, not really. I have to ssh into the Pi, after turning it on, start the app via e.g., nix run .# and then when the ssh session goes away the app goes away - yes I know I can circumvent this.
But that would not solve the "startup" problem, i.e., the Pi boots and it directly goes into running the app as it's main purpose.
systemd to the rescue - obviously.
So I wrote a systemd service config via nix:
systemd.user.services.note-sync-ui = {
enable = true;
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
wantedBy = [ "graphical-session.target" ];
description = "Display and Sync notes on the screen";
serviceConfig = {
Type = "simple";
EnvironmentFile = "/home/da-usaaah/flakes/overhead-display/env";
ExecStart = "${overhead-display.packages.${pkgs.system}.default}/bin/enter-shell";
Restart = "on-failure";
RestartSec = "5s";
};
};
I struggled a bit with this, because the rust code was not very resilient and using network-online.target does not guarantee DNS lookups work. And without Restart=on-failure the unit would just nose-dive on startup and not work. nss-lookup.target is a thing, but it's under systemctl list-units --type=target and not under --user, why I do not know, but I did not manage to "just" consume it as target. Maybe I did something wrong and I did not "research" on how to make it available to units running with --user. Why? Because adding the two lines for Restart(Sec) was infinite times more pragmatic and efficient. I am not saying there is a more optimal way and I definitely want to explore that avenue in the future, but not now.
With all of this done I can turn of the power for the Pi and Display or on as I please and once its booted my application displays and syncs PDFs all day long. Fucking awesome.
.... well
... well, except the Boox NoteAir4C Note Sync PDF Export and Okular does at least two funky things from time to time:
- The PDF export just refuses to upload the "true" / current note, i.e., today it just did not re-render page 3 of the current note. No matter what I did. Which is horrendous
- Okular/pdftk or something I have not pinned down: The pdf contents look like one of the "melting" paintings by Salvador Dali. I would have included one of these magnificent art pieces here for reference but fuck copyright, it's still not under public domain.